game_manager_lib\services\recommendation/
core.rs

1//! Estruturas e tipos fundamentais do sistema de recomendação
2//!
3//! Este módulo define as estruturas de dados e configurações centrais
4//! usadas em todo o sistema de recomendação.
5
6use crate::models::{Game, GameTag};
7use crate::utils::tag_utils::TagKey;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11// === CONSTANTES ===
12
13pub const MAX_TAG_CONTRIBUTION: f32 = 300.0; // Cap por tag affinity
14pub const WEIGHT_GENRE: f32 = 3.0;
15pub const WEIGHT_PLAYTIME_HOUR: f32 = 1.2;
16pub const WEIGHT_FAVORITE: f32 = 30.0;
17pub const WEIGHT_USER_RATING: f32 = 8.0;
18pub const WEIGHT_SERIES: f32 = 1.0;
19
20// === ESTRUTURAS ===
21
22#[derive(Debug, Deserialize, Serialize, Clone)]
23pub struct RecommendationConfig {
24    pub content_weight: f32,
25    pub collaborative_weight: f32,
26    pub age_decay: f32,
27    pub favor_series: bool,
28}
29
30impl Default for RecommendationConfig {
31    fn default() -> Self {
32        Self {
33            content_weight: 0.65,
34            collaborative_weight: 0.35,
35            age_decay: 0.98,
36            favor_series: true,
37        }
38    }
39}
40
41#[derive(Debug, Clone)]
42pub struct UserSettings {
43    pub filter_adult_content: bool,
44    pub series_limit: SeriesLimit,
45}
46
47impl Default for UserSettings {
48    fn default() -> Self {
49        Self {
50            filter_adult_content: false,
51            series_limit: SeriesLimit::Moderate,
52        }
53    }
54}
55
56#[derive(Debug, Clone, Copy)]
57pub enum SeriesLimit {
58    None,
59    Moderate,   // Max 2 em 10
60    Aggressive, // Max 1 em 10
61}
62
63#[derive(Debug, Serialize, Clone)]
64pub struct RecommendationReason {
65    pub label: String,
66    pub type_id: String,
67}
68
69#[derive(Debug, Clone)]
70pub struct GameWithDetails {
71    pub game: Game,
72    pub genres: Vec<String>,
73    pub tags: Vec<GameTag>,
74    pub series: Option<String>,
75    pub release_year: Option<i32>,
76    pub steam_app_id: Option<u32>,
77}
78
79#[derive(Debug, Serialize, Deserialize, Clone)]
80pub struct UserPreferenceVector {
81    pub genres: HashMap<String, f32>,
82    #[serde(
83        serialize_with = "serialize_tags",
84        deserialize_with = "deserialize_tags"
85    )]
86    pub tags: HashMap<TagKey, f32>,
87    pub series: HashMap<String, f32>,
88    #[serde(rename = "totalPlaytime")]
89    pub total_playtime: i32,
90    #[serde(rename = "totalGames")]
91    pub total_games: i32,
92}
93
94// Serialização customizada para tags
95fn serialize_tags<S>(tags: &HashMap<TagKey, f32>, serializer: S) -> Result<S::Ok, S::Error>
96where
97    S: serde::Serializer,
98{
99    use serde::ser::SerializeMap;
100    let mut map = serializer.serialize_map(Some(tags.len()))?;
101    for (key, value) in tags {
102        let key_string = format!("{:?}:{}", key.category, key.slug);
103        map.serialize_entry(&key_string, value)?;
104    }
105    map.end()
106}
107
108// Deserialização customizada para tags
109fn deserialize_tags<'de, D>(deserializer: D) -> Result<HashMap<TagKey, f32>, D::Error>
110where
111    D: serde::Deserializer<'de>,
112{
113    let map: HashMap<String, f32> = HashMap::deserialize(deserializer)?;
114    let mut result = HashMap::new();
115
116    for (key_string, value) in map {
117        let parts: Vec<&str> = key_string.split(':').collect();
118        if parts.len() == 2 {
119            if let Ok(category) = serde_json::from_str(&format!("\"{}\"", parts[0])) {
120                let key = TagKey::new(category, parts[1].to_string());
121                result.insert(key, value);
122            }
123        }
124    }
125
126    Ok(result)
127}
128
129// === UTILITÁRIOS ===
130
131/// Parseia o ano de lançamento de uma string de data
132pub fn parse_release_year(date_str: &str) -> Option<i32> {
133    date_str.split('-').next()?.parse().ok()
134}
135
136/// Calcula o peso de um jogo baseado no tempo jogado, favoritos e avaliação do usuário.
137pub fn calculate_game_weight(game: &Game) -> f32 {
138    let playtime_hours = (game.playtime.unwrap_or(0) / 60).min(100);
139    let mut weight = 1.0 + (playtime_hours as f32 * WEIGHT_PLAYTIME_HOUR);
140
141    if game.favorite {
142        weight += WEIGHT_FAVORITE;
143    }
144
145    if let Some(rating) = game.user_rating {
146        weight += (rating as f32) * WEIGHT_USER_RATING;
147    }
148
149    weight
150}